# -*- coding: utf-8 -*-
# @Time    : 2024/7/2 10:06
# @Author  : yujiahao
# @File    : 34_process_thread_1.py
# @description:进程和线程

"""

【进程和线程】
今天我们使用的计算机早已进入多CPU或多核时代，而我们使用的操作系统都是支持“多任务”的操作系统，这使得我们可以同时运行多个程序，
也可以将一个程序分解为若干个相对独立的子任务，让多个子任务并发的执行，从而缩短程序的执行时间，同时也让用户获得更好的体验。
因此在当下不管是用什么编程语言进行开发，实现让程序同时执行多个任务也就是常说的“并发编程”，应该是程序员必备技能之一。
为此，我们需要先讨论两个概念，一个叫进程，一个叫线程。

【概念】
进程就是操作系统中执行的一个程序，操作系统以进程为单位分配存储空间，每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据，
操作系统管理所有进程的执行，为它们合理的分配资源。进程可以通过fork或spawn的方式来创建新的进程来执行其他的任务，不过新的进程也有自己独立的内存空间，
因此必须通过进程间通信机制（IPC，Inter-Process Communication）来实现数据共享，具体的方式包括管道、信号、套接字、共享内存区等。

一个进程还可以拥有多个并发的执行线索，简单的说就是拥有多个可以获得CPU调度的执行单元，这就是所谓的线程。
由于线程在同一个进程下，它们可以共享相同的上下文，因此相对于进程而言，线程间的信息共享和通信更加容易。
当然在单核CPU系统中，真正的并发是不可能的，因为在某个时刻能够获得CPU的只有唯一的一个线程，多个线程共享了CPU的执行时间。
使用多线程实现并发编程为程序带来的好处是不言而喻的，最主要的体现在提升程序的性能和改善用户体验，今天我们使用的软件几乎都用到了多线程技术，
这一点可以利用系统自带的进程监控工具（如macOS中的“活动监视器”、Windows中的“任务管理器”）来证实。

当然多线程也并不是没有坏处，站在其他进程的角度，多线程的程序对其他程序并不友好，因为它占用了更多的CPU执行时间，导致其他程序无法获得足够的CPU执行时间；
另一方面，站在开发者的角度，编写和调试多线程的程序都对开发者有较高的要求，对于初学者来说更加困难。

Python既支持多进程又支持多线程，因此使用Python实现并发编程主要有3种方式：多进程、多线程、多进程+多线程。
"""
from multiprocessing import Process
from threading import Thread, Lock
from random import randint
from time import time, sleep

# Python中的多进程

'''
Unix和Linux操作系统上提供了fork()系统调用来创建进程，调用fork()函数的是父进程，创建出的是子进程，子进程是父进程的一个拷贝，但是子进程拥有自己的PID。
fork()函数非常特殊它会返回两次，父进程中可以通过fork()函数的返回值得到子进程的PID，而子进程中的返回值永远都是0。Python的os模块提供了fork()函数。
由于Windows系统没有fork()调用，因此要实现跨平台的多进程编程，可以使用multiprocessing模块的Process类来创建子进程，而且该模块还提供了更高级的封装，
例如批量启动进程的进程池（Pool）、用于进程间通信的队列（Queue）和管道（Pipe）等。
'''


# 下面用一个下载文件的例子来说明使用多进程和不使用多进程到底有什么差别，先看看下面的代码。从main方法调用上看


class DownloadTask(Thread):
    '''
    可以直接使用threading模块的Thread类来创建线程，但是我们之前讲过一个非常重要的概念叫“继承”，
    我们可以从已有的类创建新类，因此也可以通过继承Thread类的方式来创建自定义的线程类，然后再创建线程对象并启动线程。
    '''


def download_task(filename):
    print('开始下载%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))


# 看一下耗时（标准的装饰器）
def time_diff(func1, func2=None):
    def inner(*args, **kwargs):
        start_time = time()
        if func1:
            func1(*args, **kwargs)
        if func2:
            func2(*args, **kwargs)
        end_time = time()
        print('总共耗费了%.2f秒.' % (end_time - start_time))

    return inner


@time_diff
def download_both(file1, file2):
    download_task(file1)
    download_task(file2)


@time_diff
def download_both_parallel(file1, file2):
    '''
    通过Process类创建了进程对象，通过target参数我们传入一个函数来表示进程启动后要执行的代码，后面的args是一个元组，它代表了传递给函数的参数。
    Process对象的start方法用来启动进程，而join方法表示等待进程执行结束。运行上面的代码可以明显发现两个下载任务“同时”启动了，
    而且程序的执行时间将大大缩短，不再是两个任务的时间总和。
    '''
    p1 = Thread(target=download_task, args=(file1,))
    p2 = Thread(target=download_task, args=(file2,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()


@time_diff
def download_both_thread(file1, file2):
    '''
    在Python早期的版本中就引入了thread模块（现在名为_thread）来实现多线程编程，然而该模块过于底层，而且很多功能都没有提供，
    因此目前的多线程开发我们推荐使用threading模块，该模块对多线程编程提供了更好的面向对象的封装。
    我们把刚才下载文件的例子用多线程的方式来实现一遍。
    '''
    p1 = Process(target=download_task, args=(file1,))
    p2 = Process(target=download_task, args=(file2,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()


@time_diff
def user_class(file1, file2):
    ''' 可以通过继承Thread类的方式来创建自定义的线程类，然后再创建线程对象并启动线程。'''
    p1 = DownloadTask(target=download_task, args=(file1,))
    p2 = DownloadTask(target=download_task, args=(file2,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()


def main():
    print("单进程顺序下载:")
    download_both('Python从入门到住院.pdf', 'Peking Hot.avi')

    print('\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n')

    print("多进程，并行下载:")
    download_both_parallel('Python从入门到住院.pdf', 'Peking Hot.avi')

    print('\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n')

    print("多线程，直接应用并行下载:")
    download_both_thread('Python从入门到住院.pdf', 'Peking Hot.avi')

    print('\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n')

    print("多线程，通过继承Tread类并行下载:")
    download_both_thread('Python从入门到住院.pdf', 'Peking Hot.avi')


if __name__ == '__main__':
    main()
